Allow specifying a custom output directory
authorAlex Crichton <alex@alexcrichton.com>
Thu, 28 May 2015 22:39:20 +0000 (15:39 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Thu, 28 May 2015 22:51:02 +0000 (15:51 -0700)
This commit adds support to allow specifying a custom output directory to Cargo.
First, the `build.target-dir` configuration key is checked, and failing that the
`CARGO_TARGET_DIR` environment variable is checked, and failing that the root
package's directory joined with the directory name "target" is used.

There are a few caveats to switching target directories, however:

* If the target directory is in the current source tree, and the folder name is
  not called "target", then Cargo may walk the output directory when determining
  whether a tree is fresh.
* If the target directory is not called "target", then Cargo may look inside it
  currently for `Cargo.toml` files to learn about local packages.
* Concurrent usage of Cargo will still result in badness (#354), and this is now
  exascerbated because many Cargo projects can share the same output directory.
* The top-level crate is not cached for future compilations, so if a crate is
  built into directory `foo` and then that crate is later used as a dependency,
  it will be recompiled.

The naming limitations can be overcome in time, but for now it greatly
simplifies the crawling routines and shouldn't have much of a negative impact
other than some Cargo runtimes (which can in turn be negated by following the
"target" name convention).

Closes #482

src/cargo/core/manifest.rs
src/cargo/core/package.rs
src/cargo/ops/cargo_clean.rs
src/cargo/ops/cargo_doc.rs
src/cargo/ops/cargo_package.rs
src/cargo/ops/cargo_rustc/layout.rs
src/cargo/ops/cargo_rustc/mod.rs
src/cargo/util/config.rs
src/cargo/util/toml.rs
src/doc/config.md
tests/test_cargo_compile.rs

index 6c2c5edfc284e8afd864e893eab10e1c8fec6c9b..9b7907cc7de8d72010626dcc0ca4d4e54ce4895b 100644 (file)
@@ -14,8 +14,6 @@ use util::{CargoResult, human};
 pub struct Manifest {
     summary: Summary,
     targets: Vec<Target>,
-    target_dir: PathBuf,
-    doc_dir: PathBuf,
     links: Option<String>,
     warnings: Vec<String>,
     exclude: Vec<String>,
@@ -51,8 +49,6 @@ pub struct SerializedManifest {
     version: String,
     dependencies: Vec<SerializedDependency>,
     targets: Vec<Target>,
-    target_dir: String,
-    doc_dir: String,
 }
 
 impl Encodable for Manifest {
@@ -64,8 +60,6 @@ impl Encodable for Manifest {
                 SerializedDependency::from_dependency(d)
             }).collect(),
             targets: self.targets.clone(),
-            target_dir: self.target_dir.display().to_string(),
-            doc_dir: self.doc_dir.display().to_string(),
         }.encode(s)
     }
 }
@@ -181,7 +175,6 @@ impl Encodable for Target {
 
 impl Manifest {
     pub fn new(summary: Summary, targets: Vec<Target>,
-               target_dir: PathBuf, doc_dir: PathBuf,
                exclude: Vec<String>,
                include: Vec<String>,
                links: Option<String>,
@@ -190,8 +183,6 @@ impl Manifest {
         Manifest {
             summary: summary,
             targets: targets,
-            target_dir: target_dir,
-            doc_dir: doc_dir,
             warnings: Vec::new(),
             exclude: exclude,
             include: include,
@@ -202,14 +193,12 @@ impl Manifest {
     }
 
     pub fn dependencies(&self) -> &[Dependency] { self.summary.dependencies() }
-    pub fn doc_dir(&self) -> &Path { &self.doc_dir }
     pub fn exclude(&self) -> &[String] { &self.exclude }
     pub fn include(&self) -> &[String] { &self.include }
     pub fn metadata(&self) -> &ManifestMetadata { &self.metadata }
     pub fn name(&self) -> &str { self.package_id().name() }
     pub fn package_id(&self) -> &PackageId { self.summary.package_id() }
     pub fn summary(&self) -> &Summary { &self.summary }
-    pub fn target_dir(&self) -> &Path { &self.target_dir }
     pub fn targets(&self) -> &[Target] { &self.targets }
     pub fn version(&self) -> &Version { self.package_id().version() }
     pub fn warnings(&self) -> &[String] { &self.warnings }
@@ -225,10 +214,6 @@ impl Manifest {
     pub fn set_summary(&mut self, summary: Summary) {
         self.summary = summary;
     }
-
-    pub fn set_target_dir(&mut self, target_dir: PathBuf) {
-        self.target_dir = target_dir;
-    }
 }
 
 impl Target {
index e7ed17fa6d965a5c269e53b15070b921d25240a8..fa5bcc06b7c6b6f38b02893548f570034ec623f5 100644 (file)
@@ -76,14 +76,9 @@ impl Package {
     pub fn package_id(&self) -> &PackageId { self.manifest.package_id() }
     pub fn root(&self) -> &Path { self.manifest_path.parent().unwrap() }
     pub fn summary(&self) -> &Summary { self.manifest.summary() }
-    pub fn target_dir(&self) -> &Path { self.manifest.target_dir() }
     pub fn targets(&self) -> &[Target] { self.manifest().targets() }
     pub fn version(&self) -> &Version { self.package_id().version() }
 
-    pub fn absolute_target_dir(&self) -> PathBuf {
-        self.root().join(self.target_dir())
-    }
-
     pub fn has_custom_build(&self) -> bool {
         self.targets().iter().any(|t| t.is_custom_build())
     }
index dbbf0412c91780d96e3d06fd9d26d3393f3922c6..03b0575556dbad9c65ec102384ba85f9ecc61a28 100644 (file)
@@ -21,13 +21,13 @@ pub fn clean(manifest_path: &Path, opts: &CleanOptions) -> CargoResult<()> {
                                             opts.config));
     try!(src.update());
     let root = try!(src.root_package());
-    let manifest = root.manifest();
+    let target_dir = opts.config.target_dir(&root);
 
     // If we have a spec, then we need to delete some package,s otherwise, just
     // remove the whole target directory and be done with it!
     let spec = match opts.spec {
         Some(spec) => spec,
-        None => return rm_rf(manifest.target_dir()),
+        None => return rm_rf(&target_dir),
     };
 
     // Load the lockfile (if one's available), and resolve spec to a pkgid
@@ -52,14 +52,14 @@ pub fn clean(manifest_path: &Path, opts: &CleanOptions) -> CargoResult<()> {
     let pkgs = PackageSet::new(&[]);
     let profiles = Profiles::default();
     let cx = try!(Context::new(&resolve, &srcs, &pkgs, opts.config,
-                               Layout::at(root.absolute_target_dir()),
+                               Layout::at(target_dir),
                                None, &pkg, BuildConfig::default(),
                                &profiles));
 
     // And finally, clean everything out!
     for target in pkg.targets().iter() {
         // TODO: `cargo clean --release`
-        let layout = Layout::new(&root, opts.target, "debug");
+        let layout = Layout::new(opts.config, &root, opts.target, "debug");
         try!(rm_rf(&layout.fingerprint(&pkg)));
         let profiles = [Profile::default_dev(), Profile::default_test()];
         for profile in profiles.iter() {
index 5c2439e87510cb790239f5132e53eb1e361aac00..8090a35fe18b72b0724186156e390ae6a4cac757 100644 (file)
@@ -54,8 +54,8 @@ pub fn doc(manifest_path: &Path,
             }
         };
 
-        let path = package.absolute_target_dir().join("doc").join(&name)
-                                                    .join("index.html");
+        let target_dir = options.compile_opts.config.target_dir(&package);
+        let path = target_dir.join("doc").join(&name).join("index.html");
         if fs::metadata(&path).is_ok() {
             open_docs(&path);
         }
index ccae355dee9a8a54cf682f10b8078b17fde1ed12..07a85512e2f298774796159bc259b4cf091b4739 100644 (file)
@@ -49,7 +49,8 @@ pub fn package(manifest_path: &Path,
     }
 
     let filename = format!("package/{}-{}.crate", pkg.name(), pkg.version());
-    let dst = pkg.absolute_target_dir().join(&filename);
+    let target_dir = config.target_dir(&pkg);
+    let dst = target_dir.join(&filename);
     if fs::metadata(&dst).is_ok() { return Ok(Some(dst)) }
 
     let mut bomb = Bomb { path: Some(dst.clone()) };
@@ -174,7 +175,6 @@ fn run_verify(config: &Config, pkg: &Package, tar: &Path)
     });
     let mut new_manifest = pkg.manifest().clone();
     new_manifest.set_summary(new_summary.override_id(new_pkgid));
-    new_manifest.set_target_dir(dst.join("target"));
     let new_pkg = Package::new(new_manifest, &manifest_path, &new_src);
 
     // Now that we've rewritten all our path dependencies, compile it!
index 33ea8af692a9ee5b9e382bc89bf04f468defa6d9..58c644a82e8d4fc4250862cf33a7135025556909 100644 (file)
@@ -50,6 +50,7 @@ use std::io;
 use std::path::{PathBuf, Path};
 
 use core::Package;
+use util::Config;
 use util::hex::short_hash;
 
 pub struct Layout {
@@ -67,8 +68,9 @@ pub struct LayoutProxy<'a> {
 }
 
 impl Layout {
-    pub fn new(pkg: &Package, triple: Option<&str>, dest: &str) -> Layout {
-        let mut path = pkg.absolute_target_dir();
+    pub fn new(config: &Config, pkg: &Package, triple: Option<&str>,
+               dest: &str) -> Layout {
+        let mut path = config.target_dir(pkg);
         // Flexible target specifications often point at filenames, so interpret
         // the target triple as a Path and then just use the file stem as the
         // component for the directory name.
index d10ab0efcd75932d19437597d23eef2dececfe13..ed10aeb0fc3064649f6dc3b6c74c711c9d65cf72 100644 (file)
@@ -100,9 +100,9 @@ pub fn compile_targets<'a, 'cfg: 'a>(targets: &[(&'a Target, &'a Profile)],
     } else {
         deps.iter().find(|p| p.package_id() == resolve.root()).unwrap()
     };
-    let host_layout = Layout::new(root, None, &dest);
+    let host_layout = Layout::new(config, root, None, &dest);
     let target_layout = build_config.requested_target.as_ref().map(|target| {
-        layout::Layout::new(root, Some(&target), &dest)
+        layout::Layout::new(config, root, Some(&target), &dest)
     });
 
     let mut cx = try!(Context::new(resolve, sources, deps, config,
@@ -535,20 +535,19 @@ fn prepare_rustc(package: &Package, target: &Target, profile: &Profile,
 fn rustdoc(package: &Package, target: &Target, profile: &Profile,
            cx: &mut Context) -> CargoResult<Work> {
     let kind = Kind::Target;
-    let mut doc_dir = cx.get_package(cx.resolve.root()).absolute_target_dir();
     let mut rustdoc = try!(process(CommandType::Rustdoc, package, target, cx));
     rustdoc.arg(&root_path(cx, package, target))
            .cwd(cx.config.cwd())
            .arg("--crate-name").arg(&target.crate_name());
 
+    let mut doc_dir = cx.config.target_dir(cx.get_package(cx.resolve.root()));
     if let Some(target) = cx.requested_target() {
         rustdoc.arg("--target").arg(target);
         doc_dir.push(target);
     }
 
     doc_dir.push("doc");
-
-    rustdoc.arg("-o").arg(&doc_dir);
+    rustdoc.arg("-o").arg(doc_dir);
 
     match cx.resolve.features(package.package_id()) {
         Some(features) => {
index 7681bc0cbffd7a3b8b56ca741b5f67e3d0422ce5..b594089a546bccefc31da50561200a17f1c265d0 100644 (file)
@@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
 
 use rustc_serialize::{Encodable,Encoder};
 use toml;
-use core::MultiShell;
+use core::{MultiShell, Package};
 use ops;
 use util::{CargoResult, ChainError, internal, human};
 
@@ -30,6 +30,7 @@ pub struct Config {
     cwd: PathBuf,
     rustc: PathBuf,
     rustdoc: PathBuf,
+    target_dir: Option<PathBuf>,
 }
 
 impl Config {
@@ -51,13 +52,12 @@ impl Config {
             values_loaded: Cell::new(false),
             rustc: PathBuf::from("rustc"),
             rustdoc: PathBuf::from("rustdoc"),
+            target_dir: None,
         };
 
-        cfg.rustc = try!(cfg.get_tool("rustc"));
-        cfg.rustdoc = try!(cfg.get_tool("rustdoc"));
-        let (rustc_version, rustc_host) = try!(ops::rustc_version(cfg.rustc()));
-        cfg.rustc_version = rustc_version;
-        cfg.rustc_host = rustc_host;
+        try!(cfg.scrape_tool_config());
+        try!(cfg.scrape_rustc_version());
+        try!(cfg.scrape_target_dir_config());
 
         Ok(cfg)
     }
@@ -108,6 +108,12 @@ impl Config {
 
     pub fn cwd(&self) -> &Path { &self.cwd }
 
+    pub fn target_dir(&self, pkg: &Package) -> PathBuf {
+        self.target_dir.clone().unwrap_or_else(|| {
+            pkg.root().join("target")
+        })
+    }
+
     pub fn get(&self, key: &str) -> CargoResult<Option<ConfigValue>> {
         let vals = try!(self.values());
         let mut parts = key.split('.').enumerate();
@@ -206,6 +212,32 @@ impl Config {
         Ok(())
     }
 
+    fn scrape_tool_config(&mut self) -> CargoResult<()> {
+        self.rustc = try!(self.get_tool("rustc"));
+        self.rustdoc = try!(self.get_tool("rustdoc"));
+        Ok(())
+    }
+
+    fn scrape_rustc_version(&mut self) -> CargoResult<()> {
+        let (rustc_version, rustc_host) = try!(ops::rustc_version(&self.rustc));
+        self.rustc_version = rustc_version;
+        self.rustc_host = rustc_host;
+        Ok(())
+    }
+
+    fn scrape_target_dir_config(&mut self) -> CargoResult<()> {
+        if let Some((dir, dir2)) = try!(self.get_string("build.target-dir")) {
+            let mut path = PathBuf::from(dir2);
+            path.pop();
+            path.pop();
+            path.push(dir);
+            self.target_dir = Some(path);
+        } else if let Some(dir) = env::var_os("CARGO_TARGET_DIR") {
+            self.target_dir = Some(self.cwd.join(dir));
+        }
+        Ok(())
+    }
+
     fn get_tool(&self, tool: &str) -> CargoResult<PathBuf> {
         let var = format!("build.{}", tool);
         if let Some((tool, path)) = try!(self.get_string(&var)) {
index 37857bbb8cf3f307974c912d64c88c64a4318124..7a39c50f643cb8e47ed804f130a1a45f1803c48b 100644 (file)
@@ -519,8 +519,6 @@ impl TomlManifest {
         let profiles = build_profiles(&self.profile);
         let mut manifest = Manifest::new(summary,
                                          targets,
-                                         layout.root.join("target"),
-                                         layout.root.join("doc"),
                                          exclude,
                                          include,
                                          project.links.clone(),
index 3261e5cde17ce9ebd2fff7929db62111c503db52..b3f885c1fdfc0fc1e3f00b7f464a7f8365415ea2 100644 (file)
@@ -76,9 +76,10 @@ proxy = "..."     # HTTP proxy to use for HTTP requests (defaults to none)
 timeout = 60000   # Timeout for each HTTP request, in milliseconds
 
 [build]
-jobs = 1             # number of jobs to run by default (default to # cpus)
-rustc = "rustc"      # path to the compiler to execute
-rustdoc = "rustdoc"  # path to the doc generator to execute
+jobs = 1               # number of jobs to run by default (default to # cpus)
+rustc = "rustc"        # path to the compiler to execute
+rustdoc = "rustdoc"    # path to the doc generator to execute
+target-dir = "target"  # path of where to place all generated artifacts
 ```
 
 # Environment Variables
@@ -92,3 +93,5 @@ Cargo recognizes a few global environment variables to configure how it runs:
   compiler instead.
 * `RUSTDOC` - Instead of running `rustdoc`, Cargo will execute this specified
   `rustdoc` instance instead.
+* `CARGO_TARGET_DIR` - Location of where to place all generated artifacts,
+  relative to the current working directory.
index 0583479581b4eb96a9c49b756309b01f5fd1b7ff..d80e51aa6f6e9b353d130813bfa410de2900733c 100644 (file)
@@ -1821,3 +1821,45 @@ test!(ignore_dotfile {
     assert_that(p.cargo("build"),
                 execs().with_status(0));
 });
+
+test!(custom_target_dir {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("src/main.rs", "fn main() {}");
+    p.build();
+
+    let exe_name = format!("foo{}", env::consts::EXE_SUFFIX);
+
+    assert_that(p.cargo("build").env("CARGO_TARGET_DIR", "foo/target"),
+                execs().with_status(0));
+    assert_that(&p.root().join("foo/target/debug").join(&exe_name),
+                existing_file());
+    assert_that(&p.root().join("target/debug").join(&exe_name),
+                is_not(existing_file()));
+
+    assert_that(p.cargo("build"),
+                execs().with_status(0));
+    assert_that(&p.root().join("foo/target/debug").join(&exe_name),
+                existing_file());
+    assert_that(&p.root().join("target/debug").join(&exe_name),
+                existing_file());
+
+    fs::create_dir(p.root().join(".cargo")).unwrap();
+    File::create(p.root().join(".cargo/config")).unwrap().write_all(br#"
+        [build]
+        target-dir = "bar/target"
+    "#).unwrap();
+    assert_that(p.cargo("build").env("CARGO_TARGET_DIR", "foo/target"),
+                execs().with_status(0));
+    assert_that(&p.root().join("bar/target/debug").join(&exe_name),
+                existing_file());
+    assert_that(&p.root().join("foo/target/debug").join(&exe_name),
+                existing_file());
+    assert_that(&p.root().join("target/debug").join(&exe_name),
+                existing_file());
+});